《Linux – Linux高级编程 – 第二部分 进程与线程》第3章 进程间通信(IPC通信)

3.3 IPC 机制

在前面,我们学习了传统的进程间通信方式——无名管道(pipe)、有名管道(fifo)和信号(signal)。接下来看看更高级别的进程间通信(Inter-Process Communication, 简称IPC)。

1.共享内存(share memory);
2.信号灯(semaohore);
3.消息队列(message queue);

IPC对象是活动在内核级别的一种进程间通信的工具。存在的IPC对象通过它的标识符来引用和访问,这个标识符是一个非负整数,它唯一的标识了一个IPC对象,这个IPC对象可以是消息队列或信号量或共享存储器中的任意一种类型。

gpdrKe.png

图5

Linux系统中标识符被声明成整数,所以可能存在的最大标识符为65535。当时对于IPC对象,其标识符(id)与文件描述符有所不同,使用open函数打开一个文件时,返回的文件描述符的值为当前进程最小可用的文件描述符数组的下标。IPC对象是系统全局的流水号,其删除或创建时相应的标识符的值会不断增加到最大的值,归零循环分配使用。

IPC的标识符只解决了内部访问一个IPC对象的问题,如何让多个进程都访问某一个特定的IPC对象还需要一个外部键(key),一个IPC对象都与一个键相关联。这样就解决了多进程在一个IPC对象上汇合的问题。IPC对象时需要指定一个键值,类型为key_t,在<sys/types.h>中定义为一个长整型。键值到标识符的转换是由系统内核来维护的,这里调用IPC对象的创建函数(semget msgget shmget )实现key 值到 id 的转换。

从上图中我们可以看到得到这个键值 key 有两种方法:

1)通用方法:调用ftok()函数

函数ftok可以使用两个参数生成一个键值,函数原型如下:

表11 ftok()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
函数原型 key_t ftok(const char *pathname, int proj_id);
函数参数 pathname:文件路径
proj_id:子序列号,它是一个8bit的整数。即范围是0~255
函数返回值 成功:键值
失败:-1

函数中进行的操作是,取该文件的stat结构的st_dev成员和st_ino成员的部分值,然后与参数ID的第八位结合起来生成一个键值。由于只是使用st_dew和st_ino的部分值,所以会丢失信息,不排除两个不同文件使用同一个ID,得到同样键值的情况。在使用ftok()函数时,里面有两个参数,即fname和id,fname为指定的文件名,而id为子序列号,这个函数的返回值就是key,它与指定的文件的索引节点号和子序列号id有关,这样就会给我们一个误解,即只要文件的路径,名称和子序列号不变,那么得到的key值永远就不会变。事实上,这种认识是错误的,想想一下,假如存在这样一种情况:在访问同一共享内存的多个进程先后调用ftok()时间段中,如果fname指向的文件或者目录被删除而且又重新创建,那么文件系统会赋予这个同名文件新的i节点信息,于是这些进程调用的ftok()都能正常返回,但键值key却不一定相同了。由此可能造成的后果是,原本这些进程意图访问一个相同的共享内存对象,然而由于它们各自得到的键值不同,实际上进程指向的共享内存不再一致;如果这些共享内存都得到创建,则在整个应用运行的过程中表面上不会报出任何错误,然而通过一个共享内存对象进行数据传输的目的将无法实现。

在Ubuntu下,ftok()产生键值的原理。

表12 stat()函数
所需头文件 #include <sys/types.h>
#include <sys/stat.h>
#include
函数原型 int stat(const char path, struct stat buf);
函数参数 path:文件路径
buf:文件信息的结构体
函数返回值 成功:0
失败:-1

struct stat结构体的定义如下:
/usr/include/asm/stat.h

struct stat {  
       unsigned long  st_dev;//文件的设备编号  
       unsigned long  st_ino;//节点  
       unsigned short st_mode; //文件的类型和存取的权限  
       unsigned short st_nlink;//连到该文件的硬连接数目,刚建立的文件值为1  
       unsigned short st_uid; //用户ID  
       unsigned short st_gid; //组ID  
       unsigned long st_rdev;   
       unsigned long  st_size;  
       unsigned long st_blksize;  
       unsigned long  st_blocks;  
       unsigned long  st_atime;  
       unsigned long st_atime_nsec;  
       unsigned long  st_mtime;  
       unsigned long st_mtime_nsec;  
       unsigned long  st_ctime;  
       unsigned long st_ctime_nsec;  
       unsigned long  __unused4;  
       unsigned long  __unused5;  
};
#include <stdio.h>         
#include <stdlib.h>    
#include <sys/stat.h>  

int main()  
{  
        char    filename[50];  
        struct stat     buf;  
        int     ret;  
        strcpy( filename, "." );  
        ret = stat( filename, &buf );  
        if( ret )  
        {  
                printf( "stat error\n" );  
                return -1;  
        }  
        printf( "the file info: ftok( filename, 0x27 ) = %x, st_dev = %x, st_ino= %x\n", ftok( filename, 0x27 ),  buf.st_dev,buf.st_ino );  

        return 0;  
}

执行结果:

gpwiI1.png

通过执行结果可看出,ftok获取的键值是由ftok()函数的第二个参数的后8个bit,st_dev的后两位,st_ino的后四位构成的。

系统为每一个IPC对象保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者,每一个版本的内核各有不用的ipc_perm结构成员。
文件<sys/ipc.h> 中对其定义:

struct ipc_perm   
{  
    ey_t key;  
    id_t uid;  
    id_t gid;  
    id_t cuid;  
    id_t cgid;  
    nsigned short mode;  
    nsigned short seq;  
};  

2)父子进程之间
key 为IPC_PRIVATE,父子进程之间key值为IPC_PRIVATE。

当有了一个IPC对象的键值,如何让多个进程知道这个键,可以有多种实现的办法:
1.使用文件来做中间的通道,创建IPC对象进程,使用键IPC_PRIVATE成功建立IPC对象之后,将返回的标识符存储在一个文件中。其他进程通过读取这个标识符来引用IPC对象通信。

2.定义一个多个进程都认可的键,每个进程使用这个键来引用IPC对象,值得注意的是,创建IPC对象的进程中,创建IPC对象时如果该键值已经与一个IPC对象结合,则应该删除该IPC对象,再创建一个新的IPC对象。

3.多进程通信中,对于指定键引用一个IPC对象而言,可能不具有拓展性,并且在该键值已经被一个IPC对象结合的情况下。所以必须删除这个存在对象之后再建立一个新的。这有可能影响到其他正在使用这个对象的进程。函数ftok可以在一定程度上解决这个问题。

但IPC对象存在一些问题,主要集中在以下几点:
1.过于繁杂的编程接口,比起使用其他通信方式,IPC所要求的代码量要明显增多。
2.IPC不使用通用的文件系统,这也是饱受指责的原因。所以不能使用标准I/O操作函数来读写IPC对象。为此不得不新增加一些函数来支持必要的一些操作(例如msgget msgrev msgctl等)并且对于不同类型的IPC对象都有一系列特定的操作函数。由于IPC不使用文件描述符,所以不能使用多路I/O监控函数select及poll函数来操作IPC对象。
3.缺少的资源回收机制。由于IPC对象在使用过程中并不保存引用计数,所以当出现一个进程创建了IPC对象然后退出时,则这个对象只有在出现后面几种情况才会被释放或者删除,即由某一个进程读出消息,或者IPC的所有者或超级用户删除了这个对象。这也是IPC相对于管道或FIFO所欠缺的资源回收机制。

表13文件对象和IPC对象的对比
内存对象 文件对象 IPC对象
内核标识符 文件描述符,进程范围内唯一分配,最小可用值 IPC标识符,系统全局的流水号
进程共享名 文件系统路径 IPC键-key
创建API open,创建内核文件对象后返回对应的文件描述符 Xget:shmget/msgget/semget, 创建内核IPC对象后返回对应的IPC标识符
操作API read/write…,以文件描述符为操作对象 共享内存:shmat/shmdt/shmctrl
消息队列:msgsnd/msgrecv/msgctrl
信号量:semop/semctrl

3.4共享内存
共享内存可以说是Linux 下最快速、最有效的进程间通信方式。两个不同进程A 、B 共享内存的意思是,同一块物理内存被映射到进程A 、B 各自的进程地址空间,进程A 可以即时看到进程B 对共享内存中数据的更新;反之,进程B 也可以即时看到进程A对共享内存中数据的更新。

这里简单说下映射的概念:

Linux系统会为每个进程分配 4GB 的虚拟地址空间,一定情况下,需要将虚拟内存转换成物理内存,这就需要内存映射。为什么我们需要使用虚拟地址呢?最主要的原因是不同PC的物理内存会不一样,如果直接使用物理地址,会造成程序的移植性很差,另外虚拟地址访问内存有以下优势:
1.程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
2.程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
3.不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。

进程可用的虚拟地址范围称为该进程的“虚拟地址空间”。每个用户模式进程都有其各自的专用虚拟地址空间。对于32位进程,虚拟地址空间通常为 4 GB,范围从 0x00000000 至 0xFFFFFFFF。

3.4.1共享内存的概念

共享内存从字面意义解释就是多个进程可以把一段内存映射到自己的进程空间,以此来实现数据的共享及传输,这也是所有进程间通信方式最快的一种,共享内存是存在于内核级别的一种资源。

在Shell 中可以使用ipcs 命令查看当前系统IPC中的状态,在文件系统中/proc目录下有对其描述的相应文件。

gpwVxO.png

ipcs -m ,其中 -m 是 memory 的意思 。

在系统内核为一个进程分配内存地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存同时分配给不同的进程。共享内存机制就是通过该原理实现的,共享内存机制只是提供数据的传送,如何控制服务器端和客户端的读写操作互斥,这就需要一些其他的辅助工具,例如信号量。

采用共享内存通信的一个显而易见的好处就是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户控件进行四次数据的拷贝,而共享内存只拷贝两次数据:一次从输入文件到共享区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,知道通信完毕为止,这样,数据内同一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在接触映射时才写回文件的。因此,采用共享内存的通信方式效率是最高的。

共享内存最大不足之处在意,由于多个进程对同一块内存区域具有访问的权限,各个进程之间的同步问题显得尤为重要。必须控制同一时刻只有一个进程对共享内存区域写入数据,否则会造成数据的混乱。同步控制问题可以由信号量来解决。

对于每一个共享存储段,内核会为其维护一个shmid_ds类型的结构体,其定义在头文件<sys/shm.h>中,其定义如下:

struct shmid_ds  
{  
    struct ipc_perm shm_perm   //Operation permission structure.  
    size_t shm_segsz  //Size of segment in bytes.  
    pid_t  shm_lpid   //Process ID of last shared memory operation.  
    pid_t  shm_cpid   //Process ID of creator.  
    shmatt_t shm_nattch //Number of current attaches.  
    time_t shm_atime  //Time of last shmat                
    time_t shm_dtime  //Time of last shmdt  
    time_t shm_ctime  //Time of last change by shmctl  
 }  

3.4.2共享内存的相关操作

1)创建或打开共享内存

要使用共享内存,首先要创建一个共享内存区域,创建共享内存的函数调用如下:

表14 shmget()函数
所需头文件 #include <sys/ipc.h>
#include <sys/shm.h>
函数原型 int shmget(key_t key, size_t size, int shmflg);
函数传入值 key:IPC_PRIVATEhuo ftok的返回值
size:共享内存的大小
shmflg:同open函数的权限值
函数返回值 成功:共享内存的标识符
失败:-1

shmget函数除了可以用于创建一个新的共享内存外,也可以用于打开一个已存在的共享内存。其中,参数key表示索要创建或打开的共享内存的键值。size表示共享内存区域的大小,只在创建一个新的共享内存时生效。参数shmflag 表示调用函数的操作类型,也可用于设置共享内存的访问权限,两者通过逻辑或表示.参数key 和 flag 决定了调用函数 shmget 的作用,相应的约定如下:
1.当 key 为 IPC_PRIVATE 时,创建一个新的共享内存,此时参数 flag 的取值无效;
2.当 key 不为 IPC_PRIVATE时,且flag 设置了IPC_CREAT 位,而没有设置 IPC_EXCL 位,则执行操作由key取值决定。如果key 为内核中每个已存在的共享内存的键值。则执行打开这个键的操作;反之,则执行创建共享内存的操作;
3.当 key 不为 IPC_PRIVATE时,且flag 设置了IPC_CREAT 位和 IPC_EXCL 位,则只执行创建共享内存的操作。参数key的取值应与内核中已存在的任何共享内存的键值都不相同,否则函数调用失败,返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开共享内存的函数就可以了(即将flag 设置为 IPC_CREAT,而不设置IPC_EXCL);

2)挂载
当一个共享内存创建或打开后,某个进程如果要使用该共享内存则必须将此内存区附加到它的地址空间,附加操作的系统调用函数如下:

表15 shmat()函数
所需头文件 #include <sys/types.h>
#include <sys/shm.h>
函数原型 void shmat(int shmid, const void shmaddr, int shmflg);
函数传入值 shmid:要映射共享内存的标识符
shmaddr:将共享内存地址映射到指定地址(若为NULL,则系统自动完成映射)
shmflg:默认0,共享内存只读
函数返回值 成功:映射后的地址
失败:-1

参数shmid 指定要引入的共享内存,参数 addr 和 flag 组合说明要引入的地址值,通常将 shmaddr 设置为NULL ,shmflag为0;

3)分离
当进程对共享内存段的操作完成后,应调用 shmdt 函数,作用是将指定的共享内存段从当前进程空间中脱离出去,函数原型如下:

表16 shmdt()函数
所需头文件 #include <sys/types.h>
#include <sys/shm.h>
函数原型 int shmdt(const void *shmaddr);
函数传入值 shmaddr:共享内存映射后的地址
函数返回值 成功:0
失败:-1

此函数仅用于将共享内存区域与进程的地址空间分离,并不删除共享内存本身。参数addr是调用 shmat 函数时的返回值。

4)共享内存的控制
由于共享内存这一特殊的资源类型,使它不同于普通的文件,因此,系统需要为其提供专有的操作函数,其函数原型如下:

表17 shmct()函数
所需头文件 #include <sys/ipc.h>
#include <sys/shm.h>
函数原型 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数传入值 shmid:要操作的内存标识符
cmd:IPC_STAT获取对象属性,IPC_GET设置对象属性,IPC_RMID删除对象
buf:指定IPC_STAT/IPC_SET时用以保存/设置对象
函数返回值 成功:0
失败:-1

表18共享内存的编程模型
序号 服务进程 客户进程 动作
1 使用约定文件创建key 使用约定文件创建key ftok
2 使用key创建共享内存 使用key获取共享内存ID shmget
3 挂载到共享内存 挂载到共享内存 shmat
4 使用内存 使用内存
5 卸载共享内存 卸载共享内存 shmdt
6 释放共享内存 shmctl

共享内存的使用步骤:

 创建key,可以使用头文件定义或ftok();
 使用shmget(key, …)用key创建/获取共享内存的ID;
 使用shmat(ID)挂载(映射)共享内存;
 正常使用;
 使用shmdt()脱接(接触映射)共享内存;
 如果确保不再使用,可以删除共享内存。

下面是一个实例,两个进程间实现共享内存进行通信:

#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#define BUFFER_SIZE 2048  

int main()  
{  
    pid_t pid;  
    int shmid;  
    char *shm_addr;  
    char flag[] = "WROTE";  
    char buffer[BUFFER_SIZE];  

    if((shmid = shmget(IPC_PRIVATE,BUFFER_SIZE,0666)) < 0)  
    {  
        perror("shmget");  
        exit(-1);  
    }  
    else  
    {  
        printf("Create shared-memory:%d\n",shmid);  
    }  

    system("ipcs -m");  
    pid = fork();  
    if(pid < 0)  
    {  
        perror("fork error");  
        exit(-1);  
    }  
    else if(pid == 0)  
    {  
        shm_addr = shmat(shmid,0,0);  
        if(shm_addr == (void *)-1)  
        {  
            perror("Child:shmat");  
            exit(-1);  
        }  
        else  
        {  
            printf("Child:Attach shared-memory:%p \n",shm_addr);  
        }  

        system("ipcs -m");  

        while(strncmp(shm_addr,flag,strlen(flag)))  
        {  
            printf("Child:wait for enable data...\n");  
            sleep(5);  
        }  
        strcpy(buffer,shm_addr + strlen(flag));  
        printf("Child:shared-memory:%s\n",buffer);  

        if((shmdt(shm_addr)) < 0)  
        {  
            perror("shmdt");  
            exit(-1);  
        }  
        else  
        {  
            printf("Child: Deattach shared-memory\n");  
        }  
        system("ipcs -m");  

        if(shmctl(shmid,IPC_RMID,NULL) == -1)  
        {  
            perror("Child:shmctl(IPC_RMID)");  
            exit(-1);  
        }  
        else  
        {  
            printf("Delete shmared-memory\n");  
        }  

        system("ipcs -m");  
    }  
    else  
    {  
        if((shm_addr = shmat(shmid,0,0)) == (void *)-1)  
        {  
            perror("parent:shmat");  
            exit(-1);  
        }  
        else  
        {  
            printf("Parent:Attach shmared-memory:%p\n",shm_addr);  
        }  
        sleep(1);  
        printf("\nInput some string:\n");  
        fgets(buffer,BUFFER_SIZE,stdin);  
        strncpy(shm_addr + strlen(flag),buffer,strlen(buffer));  
        strncpy(shm_addr,flag,strlen(flag));  
        if((shmdt(shm_addr)) < 0)  
        {  
            perror("Parent:shmdt");  
            exit(-1);  
        }  
        else  
        {  
            printf("Parent : Deattach shared-memory\n");  
        }  
        system("ipcs -m");  
        waitpid(pid,NULL,0);  
        printf("Finsihed\n");  
    }  
    return 0;  
}  

执行结果如下:

gpBvxf.png
gpDpqg.png
gpDCZQ.png
gpDkin.png
gpDER0.png
gpDeMT.png
gpDmsU.png
gpDKZ4.png
gpDMdJ.png

通过结果,不断的调用 system ,可以看到共享内存区的变化。

3.5信号量

信号灯(semaphore),也叫信号量,它是不同进程间或一个给定进程内部不同线程间同步的机制。
信号灯种类:1)posix 有名信号灯 2)posix 基于内存的信号灯(无名信号灯) 3)System V 信号灯 (IPC对象);

信号灯:
1)二值信号灯:值为 0 或 1。与互斥锁类似,资源可用时值为1,不可用时值为 0;
2)计数信号灯:值在 0 到 n 之间。用来统计资源,其值代表可用资源数;
等待操作时等待信号灯的值变为大于0,然后将其减一;而释放操作则相反,用来唤醒等待资源的进程或者线程;

gpweMD.png

图6

事实上,在信号量的实际应用中,是不能单独定义一个信号量的,而只能定义一个信号量集,其中包含一组信号量,同一信号量集中的信号量使用同一个引用ID,这样的设置是为了多个资源和同步操作的需要。每个信号量集都有一个与之对应结构,一种记录了信号量集的各种信息,该结构的定义如下:

struct semid_ds  
{             
    struct ipc_perm  sem_perm  //Operation permission structure.  
    unsigned short   sem_nsems //Number of semaphores in set.  
    time_t           sem_otime //Last semop              
    time_t           sem_ctime //Last time changed by semctl  
}  

sem结构记录一个信号量的信息,其定义如下:

struct sem  
{            
    unsigned short  semval   //Semaphore value.  
    pid_t           sempid   //Process ID of last operation.  
    unsigned short  semncnt  //Number of processes waiting for semval to become greater than current value.  
    unsigned short  semzcnt  //Number of processes waiting for semval to become 0.  
} 

下面是信号量操作有关的函数调用:

函数说明:
在Linux 系统中,使用信号量通常分为以下几个步骤:
1)创建信号量或获得系统已存在的信号量,此时需要调用 semget() 函数。不同进程通过使用同一个信号键值来获得同一个信号量;
2)初始化信号量,此时使用 senctl() 函数的 SETVAL 操作。当使用二维信号量时,通常将信号量初始化为1;
3)进行信号量的PV操作,此时调用 semop() 函数。这一步是实现进程之间的同步和互斥的核心工作部分;
4)如果不需要信号量,则从系统中删除它,此时使用semctl() 函数的IPC_RMID 操作。此时需要注意,在程序中不应该出现对已经被删除的信号量的操作;

下面是具体说明:

1、创建或打开信号量集
使用函数 semget 可以创建或者获得一个信号量集ID,函数原型如下:

表19 semget()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型 int semget(key_t key, int nsems, int semflg);
函数传入值 key:和信号灯有关的key值
nsems:信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限,通常为IPC_CREAT
0666
函数返回值 成功:信号灯集ID
失败:-1

此函数可以用于创建或打开一个信号量集。其中,参数key 表示要创建或打开的信号量集对于的键值。参数 nsems 表示创建的信号量集中包含的信号量的个数,此参数只在创建一个新的信号量集时有效。参数flag表示调用函数的操作类型,也可以用于设置信号量集的访问权限,两者通过逻辑或表示。调用函数semget 的作用由参数key和flag 决定。

另外,当semget 成功创建一个新的信号量集时,它相应的semid_ds结构被初始化。ipc_perm 结构中成员被设置成相应的值 ,sem_nsems设置为函数参数nsems的值,sem_otime被设置为0,sem_ctime 设置为系统当前时间。

2、对信号量集的操作
函数semop 用以操作一个信号量集,函数原型如下:

表20 semop()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型 int semop(int semid, struct sembuf *sops, unsigned nsops);
函数传入值 semid,:信号灯集ID
struct sembuf *sops:每一个元素表示一个操作
nsops:要操作的信号灯集的个数
函数返回值 成功:0
失败:-1

此函数是一个原子操作,一旦执行就将执行数组中所有的操作;
结构体sembuf 用来说明所要执行的操作,其定义如下:

struct sembuf  
{  
    unsigned short sem_num; //要操作的信号灯的编号  
    short sem_op;   //  0: 等待,知道信号灯的值变为0  
                        //  1: 释放资源,V操作   
                        // -1: 分配资源,P操作  
    short sem_flg; //0,IPC_NOWAIT,SEM_UNDO  
}  

3、信号量集的控制
和共享内存的控制一样,信号量集也有自己的专属控制函数 semctl ,函数原型如下:

表21 semctl()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型 int semctl(int semid, int semnum, int cmd, …);
函数传入值 semid,:信号灯集ID
semnum:要修改的信号灯集编号
cmd:GETVAL获取信号灯的值,SETVAL设置信号灯的值,IPC_RMID从系统中删除信号灯集
函数返回值 成功:0
失败:-1

参数cmd 定义函数所要进行的操作,其取值及表达的意义与参数arg 的设置有关,最后一个参数arg 是一个联合体(union),其定义如下:

union semun {  
       int              val;    /* Value for SETVAL */  
       struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */  
       unsigned short  *array;  /* Array for GETALL, SETALL */  
       struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */  
};  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/sem.h>  
#define DELAY_TIME 3  

union semun   
{  
    int              val;    /* Value for SETVAL */  
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */  
    unsigned short  *array;  /* Array for GETALL, SETALL */  
    struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */  
};  

int init_sem(int sem_id,int init_value);  
int del_sem(int sem_id);  
int sem_p(int sem_id);  
int sem_v(int sem_id);  

int init_sem(int sem_id,int init_value)  
{  
    union semun sem_union;  
    sem_union.val = init_value;  
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1)  
    {  
        perror("Initialize semaphore");  
        exit(-1);  
    }  

    return 0;  
}  
int del_sem(int sem_id)  
{  
    union semun sem_union;  
    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)  
    {  
        perror("Delete semaphore");  
        return -1;  
    }  
}  
int sem_p(int sem_id)  
{  
    struct sembuf sem_b;  
    sem_b.sem_num = 0;  
    sem_b.sem_op = -1;  
    sem_b.sem_flg = SEM_UNDO;  

    if(semop(sem_id,&sem_b,1) == -1)  
    {  
        perror("P operation");  
        return -1;  
    }  

    return 0;  
}  
int sem_v(int sem_id)  
{  
    struct sembuf sem_b;  
    sem_b.sem_num = 0;  
    sem_b.sem_op = 1;  
    sem_b.sem_flg = SEM_UNDO;  

    if(semop(sem_id,&sem_b,1) == -1)  
    {  
        perror("V operation");  
        return -1;  
    }  
    return 0;  
}  
int main()  
{  
    pid_t pid;  
    int sem_id;  

    sem_id = semget(ftok(".",'a'), 1, 0666|IPC_CREAT);  
    init_sem(sem_id,0);  

    pid = fork();  
    if(pid < 0)  
    {  
        perror("fork fails");  
        exit(-1);  
    }  
    else if(pid == 0)  
    {  
        printf("Child process will wait for some seconds...\n");  
        sleep(DELAY_TIME);  
        printf("The returned value is %d in the child process(PID = %d)\n",  
                pid,getpid());  
        sem_v(sem_id);  
    }  
    else  
    {  
        sem_p(sem_id);  
        printf("The returned value is %d in the father process(PID = %d)\n",  
                pid,getpid());  
        sem_v(sem_id);  
        del_sem(sem_id);  
    }  

    return 0;  
}

执行结果如下:

gpwGz8.png

#include <unistd.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <string.h>  
#include <sys/sem.h>  

union semun  
{  
    int val;  
    struct semid_ds *buf;  
    unsigned short *arry;  
};  

static int sem_id = 0;  

static int set_semvalue();  
static void del_semvalue();  
static int semaphore_p();  
static int semaphore_v();  

int main(int argc, char *argv[])  
{  
    char message = 'X';  
    int i = 0;  

    //创建信号量  
    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);  

    if(argc > 1)  
    {  
        //程序第一次被调用,初始化信号量  
        if(!set_semvalue())  
        {  
            fprintf(stderr, "Failed to initialize semaphore\n");  
            exit(EXIT_FAILURE);  
        }  
        //设置要输出到屏幕中的信息,即其参数的第一个字符  
        message = argv[1][0];  
        sleep(2);  
    }  
    for(i = 0; i < 10; ++i)  
    {  
        //进入临界区  
        if(!semaphore_p())  
            exit(EXIT_FAILURE);  
        //向屏幕中输出数据  
        printf("%c", message);  
        //清理缓冲区,然后休眠随机时间  
        fflush(stdout);  
        sleep(rand() % 3);  
        //离开临界区前再一次向屏幕输出数据  
        printf("%c", message);  
        fflush(stdout);  
        //离开临界区,休眠随机时间后继续循环  
        if(!semaphore_v())  
            exit(EXIT_FAILURE);  
        sleep(rand() % 2);  
    }  

    sleep(10);  
    printf("\n%d - finished\n", getpid());  

    if(argc > 1)  
    {  
        //如果程序是第一次被调用,则在退出前删除信号量  
        sleep(3);  
        del_semvalue();  
    }  
    exit(EXIT_SUCCESS);  
}  

static int set_semvalue()  
{  
    //用于初始化信号量,在使用信号量前必须这样做  
    union semun sem_union;  

    sem_union.val = 1;  
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1)  
        return 0;  
    return 1;  
}  

static void del_semvalue()  
{  
    //删除信号量  
    union semun sem_union;  

    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)  
        fprintf(stderr, "Failed to delete semaphore\n");  
}  

static int semaphore_p()  
{  
    //对信号量做减1操作,即等待P(sv)  
    struct sembuf sem_b;  
   sem_b.sem_num = 0;  
    sem_b.sem_op = -1;//P()  
    sem_b.sem_flg = SEM_UNDO;  
    if(semop(sem_id, &sem_b, 1) == -1)  
    {  
        fprintf(stderr, "semaphore_p failed\n");  
        return 0;  
    }  
    return 1;  
}  

static int semaphore_v()  
{  
    //这是一个释放操作,它使信号量变为可用,即发送信号V(sv)  
    struct sembuf sem_b;  
    sem_b.sem_num = 0;  
    sem_b.sem_op = 1;//V()  
    sem_b.sem_flg = SEM_UNDO;  
    if(semop(sem_id, &sem_b, 1) == -1)  
    {  
        fprintf(stderr, "semaphore_v failed\n");  
        return 0;  
    }  
    return 1;  
}

结果如下所示:

gpwszT.png

3.6消息队列

消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免有名管道的同步和阻塞问题。但是消息队列与有名管道一样,每个数据块都有一个最大长度的限制。对消息队列有写权限的进程可以向其中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。

表22 msgget()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型 int msgget(key_t key, int msgflg);
函数参数 key:键值,可以认为是一个端口号,也可以由函数ftok生成。
msgflg:权限标志,表示消息队列的访问权限,它与文件的访问权限一样。IPC_CREAT值,若没有该队列,则创建一个并返回新标识符;若已存在,则返回原标识符。
IPC_EXCL值,若没有该队列,则返回-1;若已存在,则返回0。
函数返回值 成功:返回一个正整数,即队列标识;
失败:-1

该函数用来创建和访问一个消息队列。

表23 msgsnd()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
函数参数 msqid:msgid是由msgget函数返回的消息队列标识符
msgp:msgp是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msgp所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。
msgsz:msg_sz是msgp指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msgsz是不包括长整型消息类型成员变量的长度。
msgflg:用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
函数返回值 成功:0
失败:-1

该函数用来把消息添加到消息队列中。

表24 msgrcv()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
函数参数 msqid:msgid是由msgget函数返回的消息队列标识符
msgp:msgp是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msgp所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。
msgtyp:可以实现一种简单的接收优先级。如果msgtyp为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtyp的绝对值的第一个消息。
msgsz:msgsz是msgp指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msgsz是不包括长整型消息类型成员变量的长度。
msgflg:用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
函数返回值 成功:0
失败:-1

该函数用来从一个消息队列获取消息。

表25 msgctl()函数
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
函数参数 msqid:msgid是由msgget函数返回的消息队列标识符
cmd:cmd是将要采取的动作。
buf:buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。
函数返回值 成功:0
失败:-1

该函数用来控制消息队列,它与共享内存的shmctl函数相似。

【msg_send.c】

#include <stdio.h>   
#include <sys/types.h>   
#include <sys/ipc.h>   
#include <sys/msg.h>   
#include <errno.h>   
#define MSGKEY 1024   
struct msgstru  
{  
   long msgtype;  
   char msgtext[2048];   
};  
void main()  
{ 
    struct msgstru msgs;  
    int msg_type;  
    char str[256];  
    int ret_value;  
    int msqid;  

    msqid=msgget(MSGKEY,IPC_EXCL);  /*检查消息队列是否存在*/  
    if(msqid < 0)
    {
        msqid = msgget(MSGKEY,IPC_CREAT|0666);/*创建消息队列*/  

        if(msqid <0)
        {
            printf("failed to create msq | errno=%d [%s]\n",errno,strerror(errno));  
            exit(-1);  
        }  
    }   
    while (1)
    {  
        printf("input message type(end:0):");  
        scanf("%d",&msg_type);  
        if (msg_type == 0)  
            break;  
        printf("input message to be sent:");  
        scanf ("%s",str);  
        msgs.msgtype = msg_type;  
        strcpy(msgs.msgtext, str);  
        /* 发送消息队列 */  
        ret_value = msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT);  
        if ( ret_value < 0 ) 
        {
            printf("msgsnd() write msg failed,errno=%d[%s]\n",errno,strerror(errno));  
            exit(-1);  
        }
    }  
    msgctl(msqid,IPC_RMID,0); //删除消息队列   
}

【msg_receive.c】

#include <stdio.h>   
#include <sys/types.h>   
#include <sys/ipc.h>   
#include <sys/msg.h>   
#include <errno.h>   
#define MSGKEY 1024   
struct msgstru  
{
    long msgtype;  
    char msgtext[2048];  
};  
/*子进程,监听消息队列*/  
void childproc()
{
    struct msgstru msgs;  
    int msgid,ret_value;  
    char str[512];  

    while(1)
    {
        msgid = msgget(MSGKEY,IPC_EXCL );/*检查消息队列是否存在 */  
        if(msgid < 0)
        {
            printf("msq not existed! errno=%d [%s]\n",errno,strerror(errno));  
            sleep(2);  
            continue; 
        }  
     /*接收消息队列*/  
     ret_value = msgrcv(msgid,&msgs,sizeof(struct msgstru),0,0);  
     printf("text=[%s] pid=[%d]\n",msgs.msgtext,getpid());  
    }  
    return;  
}  
void main()  
{
    int i,cpid;  
  /* create 5 child process */  
    for (i=0;i<5;i++)
    {
        cpid = fork();  
        if (cpid < 0)  
        printf("fork failed\n");
        else if (cpid ==0) /*child process*/  
        childproc();  
    }  
}

Related posts

Leave a Comment